package com.atguigu.gulimall.ware.service.impl;

import com.atguigu.common.exception.NoStockException;
import com.atguigu.common.to.OrderTo;
import com.atguigu.common.to.mq.StockDetailTo;
import com.atguigu.common.to.mq.StockLockedTo;
import com.atguigu.common.utils.R;
import com.atguigu.common.vo.OrderVo;
import com.atguigu.gulimall.ware.dao.WareOrderDao;
import com.atguigu.gulimall.ware.entity.WareOrderEntity;
import com.atguigu.gulimall.ware.entity.WareOrderTaskDetailEntity;
import com.atguigu.gulimall.ware.entity.WareOrderTaskEntity;
import com.atguigu.gulimall.ware.feign.OrderFeginService;
import com.atguigu.gulimall.ware.feign.ProductFeginService;
import com.atguigu.gulimall.ware.service.WareOrderTaskDetailService;
import com.atguigu.gulimall.ware.service.WareOrderTaskService;
import com.atguigu.gulimall.ware.vo.OrderItemVo;
import com.atguigu.gulimall.ware.vo.SkusHasStockVo;
import com.atguigu.gulimall.ware.vo.WareSkuLockVo;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.atguigu.common.utils.PageUtils;
import com.atguigu.common.utils.Query;

import com.atguigu.gulimall.ware.dao.WareSkuDao;
import com.atguigu.gulimall.ware.entity.WareSkuEntity;
import com.atguigu.gulimall.ware.service.WareSkuService;
import org.springframework.transaction.annotation.Transactional;


@Service("wareSkuService")
public class WareSkuServiceImpl extends ServiceImpl<WareSkuDao, WareSkuEntity> implements WareSkuService {
    @Autowired
    private WareSkuDao wareSkuDao;
    @Autowired
    private ProductFeginService productFeginService;
    @Autowired
    private WareOrderTaskService orderTaskService;
    @Autowired
    private WareOrderTaskDetailService orderTaskDetailService;
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Autowired
    private OrderFeginService orderFeginService;
    @Autowired
    private WareOrderDao wareOrderDao;

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        QueryWrapper<WareSkuEntity> wrapper = new QueryWrapper<>();

        String skuId = (String) params.get("skuId");
        if (!StringUtils.isEmpty(skuId)){
            wrapper.eq("sku_id",skuId);
        }
        String wareId = (String) params.get("wareId");
        if (!StringUtils.isEmpty(wareId)){
            wrapper.eq("ware_id",wareId);
        }
        IPage<WareSkuEntity> page = this.page(new Query<WareSkuEntity>().getPage(params),wrapper);
        return new PageUtils(page);
    }

    /**
     * 保存采购项到商品库存
     * @param skuId
     * @param wareId
     * @param skuNum
     */
    @Override
    public void addStock(Long skuId, Long wareId, Integer skuNum) {
//        1、判断还没有这个库存的记录新增
        List<WareSkuEntity> entities = wareSkuDao.selectList(new QueryWrapper<WareSkuEntity>().eq("sku_id", skuId)
                .eq("ware_id", wareId));
//        如果是空的，说明是新建的库存
        if (null == entities || entities.size() == 0){
            WareSkuEntity skuEntity = new WareSkuEntity();
            skuEntity.setSkuId(skuId);
            skuEntity.setStock(skuNum);
            skuEntity.setWareId(wareId);
            skuEntity.setStockLocked(0);
            //TODO 远程查询sku的名字，如果失败，整个事务无需回滚
            //1、自己catch异常
            //TODO 还可以用什么办法让异常出现以后不回滚？高级
            try {
                R info = productFeginService.info(skuId);
                Map<String,Object> data = (Map<String, Object>) info.get("skuInfo");

                if(info.getCode() == 0){
                    skuEntity.setSkuName((String) data.get("skuName"));
                }
            }catch (Exception e){

            }
            wareSkuDao.insert(skuEntity);
        }else {
//            修改库存的量
            wareSkuDao.addStock(skuId,wareId,skuNum);
        }
    }

    /**
     * 根据skuIds查询库存
     * @param skuIds
     * @return
     */
    @Override
    public List<SkusHasStockVo> getSkusHasStock(List<Long> skuIds) {
        List<SkusHasStockVo> collect = skuIds.stream().map(skuId -> {
            SkusHasStockVo skusHasStockVo = new SkusHasStockVo();
            Long count = baseMapper.getSkusHasStock(skuId);
            skusHasStockVo.setSkuId(skuId);
//            true-->有库存  false-->无库存
            skusHasStockVo.setHasStock(count==null?false:count>0);
            return skusHasStockVo;
        }).collect(Collectors.toList());
        return collect;
    }

    /**
     * 锁定库存
     * 默认只要是运行时异常，都会回滚
     * 为某个订单锁定库存
     * 解锁库存要考虑以下场景：
     *  1、下单成功，订单过期没有支付，被系统自动取消了，或者被用户手动取消订单，库存系统都需要解锁库存
     *  2、下订单成功，库存锁定成功，但是接下来的其他业务调用失败，导致订单回滚，
     *      之前锁定的库存就要一段时间过后自动解锁，而这个解说如果要用分布式事务seata来做太慢了
     *      所以采用MQ的可靠消息+最终一致性方案（异步确保型）
     * @param vo
     * @return
     */
    @Transactional
    @Override
    public Boolean orderLockStock(WareSkuLockVo vo) {
        /**
         * 库存一进来，准备锁库存，先保存库存工作单的详情，即那个订单号和订单id准备锁库存
         * 引入这个工作单是为了追溯哪个仓库那一块商品锁了多少，包括一旦出了问题，可以人为的回滚也方便
         */
        WareOrderTaskEntity taskEntity = new WareOrderTaskEntity();
        // 相当于是给哪个订单号锁的库存
        taskEntity.setOrderSn(vo.getOrderSn());
        orderTaskService.save(taskEntity); //保存订单的库存工作单

        // 1、找到每个商品在哪个仓库有库存
        List<OrderItemVo> locks = vo.getLocks();
        List<SkuWareHasStock> collect = locks.stream().map(item -> {
            SkuWareHasStock stock = new SkuWareHasStock();
            Long skuId = item.getSkuId();//想要锁定的商品id
            stock.setSkuId(skuId);
            stock.setNum(item.getCount());
            // 查询这个商品在哪里有库存
            List<Long> wareIds = wareSkuDao.listWareIdHasSkuStock(skuId);
            stock.setWareId(wareIds);
            return stock;
        }).collect(Collectors.toList());
        // 2、锁定库存核心方法
        for (SkuWareHasStock hasStock : collect) {
            // 先查找这些商品在哪个仓库有库存。默认是没有锁住的
            Boolean skuStocked = false;
            Long skuId = hasStock.getSkuId();
            List<Long> wareIds = hasStock.getWareId();
            // 遍历每一个仓库
            if (wareIds == null && wareIds.size() == 0){
                throw new NoStockException(skuId);
            }
            // 如果仓库有库存就进行锁定仓库并进行减扣
            for (Long wareId : wareIds) {
                // 锁的商品是哪个，想要锁住哪个仓库下的，锁几件，成功返回1，否则返回0
                Long count = wareSkuDao.lockSkuStock(skuId,wareId,hasStock.getNum());
                if (count == 1){
                    // 说明锁住了，就没有必要去锁其他仓库
                    // 然后发送一个消息告诉MQ库存锁定成功了，然后触发自动解锁逻辑，一旦锁定成功，就保存这个锁成功的详情
                    /**
                     *参数说明：
                     *  库存详情工作单的id（数据库自动升序）
                     *  哪个商品id
                     *  商品名字没有传
                     *  锁几件商品
                     *  工作单id
                     *  在哪个仓库锁了商品
                     *  锁定状态，默认已锁定为1，也即是锁定成功
                     */
                    WareOrderTaskDetailEntity detailEntity = new WareOrderTaskDetailEntity(null,skuId,"",hasStock.getNum(),taskEntity.getId(),wareId,1);
//                    只要有一个锁定成功，就把工作单id外还把当前锁定的详情信息发送给MQ（防止数据回滚找不到数据）
                    orderTaskDetailService.save(detailEntity);
                    /**
                     * 保存完了以后，需要发送给MQ，锁定了哪些库存
                     *  参数一：发送给哪个交换机
                     *  参数二：用的路右键是哪个
                     *  参数三：发的消息内容是什么，发的是哪个库存工作单和锁了几件商品
                     */
                    StockLockedTo lockedTo = new StockLockedTo();
                    lockedTo.setId(taskEntity.getId());
                    StockDetailTo stockDetailTo = new StockDetailTo();
                    BeanUtils.copyProperties(detailEntity,stockDetailTo);
                    lockedTo.setDetail(stockDetailTo);
                    // 发送给MQ
                    rabbitTemplate.convertAndSend("stock-event-exchange","stock.locked",lockedTo);
                    skuStocked=true;
                    break;//直接跳出循环，锁住了一个仓库，说明库存足够
                }else{
//                    当前仓库没库存，重试下一个仓库
                }
            }
            if (skuStocked == false){
//                当前商品所有库存都没有锁住（该商品没了）
                throw new NoStockException(skuId);
            }
        }
//        3、来到了这里说明肯定是锁定成功的。
        return true;
    }

    /**
     * 只要解锁库存的消息失败，一定要告诉服务器此次解锁失败，让服务器（MQ）这个消息不要删除，可以用重新解锁
     * 需要手动的启动ACk机制
     * 解锁库存的方法
     * 幂等的
     * @param to
     */
    @Override
    public void unlockStock(StockLockedTo to) {
        //每一个工作单的详细信息
        StockDetailTo detail = to.getDetail();
//        工作单详情id
        Long detailId = detail.getId();
//        1、查询数据库关于这个订单的锁定库存信息
        /**
         * 如果数据库有，就需要解锁
         * 如果数据库没有，也就是说工作单都没有，那么库存锁定失败，整个库存也回滚了导致的没有，这种情况无需解锁。
         * 解锁订单情况：
         *  1、没有这个订单，必须解锁
         *  2、有这个订单，不是解锁库存
         * 先看订单状态：已取消订单，解锁库存
         * 如果是没有取消，就不能解锁库存
         */
        WareOrderTaskDetailEntity byId = orderTaskDetailService.getById(detailId); //查看工作单详情信息
        if (null != byId){
//            解锁，如果库存工作单在数据库有，那么说明库存是成功的，但是也不能一上来就解锁，如果有的话，只能表明库存服务自己在锁库存的时候成功了，一切成功返回出去
//            但是要看整个订单成功没有
            Long id = to.getId();//根据这个id查询订单详情信息
            WareOrderTaskEntity orderTaskEntity = orderTaskService.getById(id);
            String orderSn = orderTaskEntity.getOrderSn();// 根据订单号查询订单状态，只要订单是取消状态，才能解锁库存
//            在解锁之前，远程查询订单状态
//            R r = orderFeginService.getOrderStatus(orderSn);
//            Integer orderStatus = orderFeginService.getOrderStatus(orderSn);
            Integer orderStatus = wareSkuDao.getStatusByOrderSn(orderSn);
            /**
             *  这个订单是取消状态就解锁库存
             *  就算订单不存在，原因就是库存服务已经锁好库存了，库存工作单的数据都有，但是订单调用了其他方法
             *  订单出现问题了，导致订单回滚了，此时这种情况还要解锁库存
              */
            // 订单不存在，或者订单被取消这两种情况都得解锁库存
            if (orderStatus == null || orderStatus == 4){
                // 解锁库存成功，当前库存状态是1的话也就属于已锁定的话，但是未解锁才能解锁
                if (byId.getLockStatus() == 1){
                    // 如果当前的这个锁定状态不是已解锁的话，才能解锁
                    // 那什么时候才能解锁呢？
                    // 要在库存工作单的状态是已锁定状态才能解锁，如果已解锁过了就不需要解锁了，或者是已经把库存都真的扣减了，那就更加不能解锁了
                    unlockStock(detail.getSkuId(),detail.getWareId(),detail.getSkuNum(),detailId);
                }
            }
        }else{
//            如果没有就无需解锁，相当于消息也无需消费了
//            如果没有的话就说明库存自己本身不成功,自己都回滚了,所以不需要自动解锁;
        }
    }

    /**
     * 订单已被关闭的处理
     *  防止订单卡顿服务，导致订单状态消息一直改不了，库存消息优先到期，结果查订单状态肯定新建状态，什么都不做就走了（也即消费了）
     *  导致这个订单卡顿的订单，永远不能解锁库存
     *  处理：订单即使取消了，也给MQ发一条消息到库存服务，让他来解锁库存
     * @param orderTo
     */
    @Transactional
    @Override
    public void unlockStock(OrderTo orderTo) {
        String orderSn = orderTo.getOrderSn();
        // 解库存，按照库存工作单订单号找到库存工作单的id，再来找到之前哪些商品已经被锁定了，而且确定这个商品没有被解锁过
        // 假设这个订单都是这个库存服务都帮忙解锁过了，再来解锁一遍这就造成了重复扣库存
        // 所以需要查询库存最新解锁状态才能防止重复解锁库存
        WareOrderTaskEntity taskEntity = orderTaskService.getOrderTaskByOrderSn(orderSn);
        Long id = taskEntity.getId();
        // 拿到库存工作单的id，找到当时所有没有解锁的库存商品进行解锁
        // 1的表示新建进来的，还没有解锁，如果解锁了就变成2
        List<WareOrderTaskDetailEntity> entities = orderTaskDetailService.list(
                new QueryWrapper<WareOrderTaskDetailEntity>()
                        .eq("task_id", id)
                        .eq("lock_status", 1));
        // 调用解锁方法
        for (WareOrderTaskDetailEntity entity : entities) {
            unlockStock(entity.getSkuId(),entity.getWareId(),entity.getSkuNum(),entity.getId());
        }
    }

    @Override
    public void createOrder(OrderTo orderTo) {
        WareOrderEntity wareOrderEntity = new WareOrderEntity();
        wareOrderEntity.setOrderSn(orderTo.getOrderSn());
        wareOrderEntity.setStatus(orderTo.getStatus());
        wareOrderDao.insert(wareOrderEntity);
    }

    @Override
    public void updateOrder(OrderTo orderTo) {
        WareOrderEntity wareOrderEntity = new WareOrderEntity();
        wareOrderEntity.setOrderSn(orderTo.getOrderSn());
        wareOrderEntity.setStatus(orderTo.getStatus());
        wareOrderDao.updateByOrder(wareOrderEntity.getOrderSn(),wareOrderEntity.getStatus());
    }

    /**
     * 库存解锁方法
     * @param skuId 解锁的哪个商品
     * @param wareId 当时在哪个仓库扣减的库存
     * @param skuNum 扣减的数量
     * @param taskDetailId 库存工作单的id
     */
    private void unlockStock(Long skuId, Long wareId, Integer skuNum, Long taskDetailId) {
//        库存解锁
        wareSkuDao.unLockStock(skuId,wareId,skuNum);
        WareOrderTaskDetailEntity entity = new WareOrderTaskDetailEntity();
        entity.setId(taskDetailId);
        entity.setLockStatus(2); //已解锁状态
        orderTaskDetailService.updateById(entity);
    }


    /**
     * 库存自动解锁
     */

    @Data
    class SkuWareHasStock{
        private Long skuId;//当前商品id
        private Integer num;//锁多少件
        private List<Long> wareId;//都在哪些仓库有库存
    }

}